package utils

import (
	"encoding/json"
	"fmt"
	"github.com/gomodule/redigo/redis"
	"imooc.com/demo-lottery/comm"
	"imooc.com/demo-lottery/conf"
	"imooc.com/demo-lottery/datasource"
	"imooc.com/demo-lottery/models"
	"imooc.com/demo-lottery/services"
	"log"
	"time"
)

const giftPoolKey = "gift_pool"

// 本地开发测试的时候，每次重新启动，奖品池自动归零
func init() {
	resetServGiftPool()
}

// 重置集群的奖品池
func resetServGiftPool() {
	cacheObj := datasource.InstanceCache()
	_, err := cacheObj.Do("DEL", giftPoolKey)
	if err != nil {
		log.Println("prizedata.resetServGiftPool DEL error=", err)
	}
}

// 获取当前奖品池中的奖品数量
func GetGiftPoolNum(id int) int {
	rds := datasource.InstanceCache()
	rs, err := rds.Do("HGET", giftPoolKey, id)
	if err != nil {
		log.Println("prizedata.GetGiftPoolNum error=", err)
		return 0
	} else {
		num := comm.GetInt64(rs, 0)
		return int(num)
	}
}

// 从奖品池发奖
func prizeServiceGift(id int) bool {
	rds := datasource.InstanceCache()
	rs, err := rds.Do("HINCRBY", giftPoolKey, id, -1)
	if err != nil {
		log.Println("prizedata.prizeServiceGift error=", err)
		return false
	}
	num := comm.GetInt64(rs, -1)
	if num >= 0 {
		return true
	}
	return false
}

// 发奖是否成功，成功返回true
func PrizeGift(id, leftNum int) bool {
	ok := false
	ok = prizeServiceGift(id)

	// 如果奖品池发奖成功，才有数据库的数量减少
	if ok {
		giftService := services.NewGiftService()
		rows, err := giftService.DecrLeftNum(id, 1)
		// rows: 影响的行数
		if rows < 1 || err != nil {
			log.Println("prizedata.PrizeGift "+
				"giftService.DecrLeftNum error=", err, "rows=", rows)
			return false
		}
	}
	return ok
}

// 从奖品池发优惠券的实现
func PrizeCodeDiff(id int, codeService services.CodeService) string {
	return prizeServiceCodeDiff(id, codeService)
}

func ImportCacheCodes(id int, code string) bool {
	key := fmt.Sprintf("gift_code_%d", id)
	rds := datasource.InstanceCache()
	_, err := rds.Do("SADD", key, code)
	if err != nil {
		log.Println("prizedata.ImportCacheCodes "+
			"SADD error=", err)
		return false
	}
	return true
}

func RecacheCodes(id int, codeService services.CodeService) (sucNum, errNum int) {
	list := codeService.Search(id)
	if list == nil || len(list) == 0 {
		return 0, 0
	}
	key := fmt.Sprintf("gift_code_%d", id)
	// 重新整理时临时key，这样不影响原key的使用，最后再把tmpKey覆盖key
	tmpKey := "tmp_" + key
	rds := datasource.InstanceCache()
	for _, data := range list {
		if data.SysStatus == 0 {
			code := data.Code
			_, err := rds.Do("SADD", tmpKey, code)
			if err != nil {
				log.Println("prizedata.RecacheCodes "+
					"SADD error=", err)
				errNum++
			} else {
				sucNum++
			}
		}
	}
	_, err := rds.Do("RENAME", tmpKey, key)
	if err != nil {
		log.Println("prizedata.RecacheCodes "+
			"RENAME error=", err)
		errNum++
	}
	return
}

func GetCacheCodeNum(id int,
	codeService services.CodeService) (dbNum, cacheNum int) {

	// 统计db可用优惠券编码数量
	list := codeService.Search(id)
	if len(list) > 0 {
		for _, data := range list {
			if data.SysStatus == 0 {
				dbNum++
			}
		}
	}

	// 统计cache优惠券key数量
	key := fmt.Sprintf("gift_code_%d", id)
	rds := datasource.InstanceCache()
	rs, err := rds.Do("SCARD", key)
	if err != nil {
		log.Println("prizedata.GetCacheCodeNum "+
			"SCARD error=", err)
	} else {
		cacheNum = int(comm.GetInt64(rs, 0))
	}
	return
}

// 从奖品池发优惠券的实现
func prizeServiceCodeDiff(id int,
	codeService services.CodeService) string {

	key := fmt.Sprintf("gift_code_%d", id)
	rds := datasource.InstanceCache()
	rs, err := rds.Do("SPOP", key)
	if err != nil {
		log.Println("prizedata.prizeServiceCodeDiff "+
			"SPOP error=", err)
		return ""
	}
	code := comm.GetString(rs, "")
	if code == "" {
		log.Println("prizedata.prizeServiceCodeDiff "+
			"rs=", rs)
		return ""
	}
	codeService.UpdateByCode(&models.LtCode{
		Code:       code,
		SysStatus:  2,
		SysUpdated: comm.NowUnix(),
	}, nil)
	return code
}

// 从数据库发优惠券的实现
func prizeLocalCodeDiff(id int, codeService services.CodeService) string {
	// 保证不和正数的uid重叠
	lockUid := 0 - id - 100000000
	LockLucky(lockUid)
	defer UnlockLucky(lockUid)

	codeId := 0
	codeInfo := codeService.NextUsingCode(id, codeId)
	if codeInfo != nil && codeInfo.Id > 0 {
		codeInfo.SysStatus = 2
		codeInfo.SysUpdated = comm.NowUnix()
		codeService.Update(codeInfo, nil)
	} else {
		log.Println("prizedata.prizeCodeDiff "+
			"num codeInfo,gift_id=", id)
		return ""
	}

	return codeInfo.Code
}

// 重置一个奖品的发奖周期信息
// 奖品剩余数量也会重新设置为当前奖品数量
// 奖品的奖品池有效数量则会设置为空
// 奖品数量、发放周期等设置有修改的时候，也需要重置
// 【难点】根据发奖周期，重新更新发奖计划
func ResetGiftPrizeData(giftInfo *models.LtGift,
	giftService services.GiftService) {

	if giftInfo == nil || giftInfo.Id < 1 {
		return
	}

	id := giftInfo.Id
	nowTime := comm.NowUnix()
	// 不能发奖，不需要设置发奖周期
	if giftInfo.SysStatus == 1 || // 状态不对
		giftInfo.TimeBegin >= nowTime || // 开始时间不对
		giftInfo.TimeEnd <= nowTime || // 结束时间不对
		giftInfo.LeftNum <= 0 || // 剩余数不足
		giftInfo.PrizeNum <= 0 { // 总数不限制
		if giftInfo.PrizeData != "" {
			//清空旧的发奖计划
			clearGiftPrizeData(giftInfo, giftService)
		}
		return
	}
	dayNum := giftInfo.PrizeTime
	// 用day<=0，表示不需要发奖周期，剩余的都能进奖品池
	if dayNum <= 0 {
		setGiftPool(id, giftInfo.LeftNum)
		return
	}
	// 重置发奖计划数据
	// (1)先把奖品池清空
	setGiftPool(id, 0)
	// (2)实际的奖品计划分布计算
	prizeNum := giftInfo.PrizeNum
	avgNum := prizeNum / dayNum
	// 每天可以分配的奖品数
	dayPrizeNum := make(map[int]int)
	if avgNum >= 1 {
		for day := 0; day < dayNum; day++ {
			dayPrizeNum[day] = avgNum
		}
	}
	// 剩下的随机分配到任意一天
	prizeNum -= dayNum * avgNum //剩余的奖品数量
	for prizeNum > 0 {
		prizeNum--
		day := comm.Random(dayNum)
		dayPrizeNum[day]++
	}
	// 每天的map，每小时的map，60分钟的数组，奖品数
	prizeData := make(map[int]map[int][60]int)
	for day, num := range dayPrizeNum {
		//计算出来这一天的发奖计划
		dayPrizeData := getGiftPrizeDataOneDay(num)
		prizeData[day] = dayPrizeData
	}
	// 将周期内每天，每小时，每分钟的数据，格式化为（[具体时间:数量]）
	datalist := formatGiftPrizeData(nowTime, dayNum, prizeData)
	str, err := json.Marshal(datalist)
	if err != nil {
		log.Println("prizedata.ResetGiftPrizeData json error=", err)
	} else {
		// 保存奖品的发奖计划数据
		info := &models.LtGift{
			Id:         giftInfo.Id,
			LeftNum:    giftInfo.PrizeNum,
			PrizeData:  string(str),
			PrizeBegin: nowTime,
			PrizeEnd:   nowTime + dayNum*86400,
			SysUpdated: nowTime,
		}
		err := giftService.Update(info, nil)
		if err != nil {
			log.Println("prizedata.ResetGiftPrizeData "+
				"giftService.Update info=", info, ",error=", err)
		}
	}
}

// 清空旧的发奖计划数据
func clearGiftPrizeData(giftInfo *models.LtGift,
	giftService services.GiftService) {

	info := &models.LtGift{
		Id:        giftInfo.Id,
		PrizeData: "",
	}
	err := giftService.Update(info, []string{"prize_data"})
	if err != nil {
		log.Println("prizedata.clearGiftPrizeData "+
			"giftService.Update info=", info, ",error=", err)
	}
	setGiftPool(giftInfo.Id, 0)
}

// 设置奖品池的库存数量
func setGiftPool(id int, num int) {
	rds := datasource.InstanceCache()
	_, err := rds.Do("HSET", giftPoolKey, id, num)
	if err != nil {
		log.Println("prizedata.setGiftPool error=", err)
	}
}

// 计算出来一天的发奖计划
func getGiftPrizeDataOneDay(num int) map[int][60]int {
	rs := make(map[int][60]int)
	hourData := [24]int{}
	// 奖品数量多的时候，直接按照百分比计算出来
	// 分别将奖品分布到24个小时内
	if num > 100 {
		hourNum := 0
		for _, h := range conf.PrizeDataRandomDayTime {
			// 统计每个小时获奖的概率
			hourData[h]++
		}
		for h := 0; h < 24; h++ {
			// 获奖概率 * 总数 = 每个小时奖品数量
			d := hourData[h]
			n := d / 100 * num
			hourData[h] = n
			hourNum += n
		}
		num -= hourNum
	}
	// 奖品数量少的时候，或者剩下了一些没有分配，需要用到随即概率来计算
	for num > 0 {
		num--
		hourIndex := comm.Random(100)
		h := conf.PrizeDataRandomDayTime[hourIndex]
		hourData[h]++
	}
	// 将每小时内奖品数量分配到60分钟
	for h, hnum := range hourData {
		if hnum <= 0 {
			continue
		}
		minuteData := [60]int{}
		if hnum >= 60 {
			avgMinute := hnum / 60
			for i := 0; i < 60; i++ {
				minuteData[i] = avgMinute
			}
			hnum -= avgMinute * 60
		}
		for hnum > 0 {
			hnum--
			m := comm.Random(60)
			minuteData[m]++
		}
		rs[h] = minuteData
	}
	return rs
}

// 每天，每小时，每分钟的奖品数量，格式化为 [具体时间:数量] 格式
// 将结构为： [day][hour][minute]num 格式化,输出result：[][具体时间:数量]
// 这个格式转换最后的结果是后延的，比如1个3天的计划，从nowTime是18点，算出来后是3天后的18点结束
func formatGiftPrizeData(nowTime, dayNum int,
	prizeData map[int]map[int][60]int) (rs [][2]int) {

	nowHour := time.Now().Hour()
	// 处理日期的数据
	for dn := 0; dn < dayNum; dn++ {
		dayData, ok := prizeData[dn]
		if !ok {
			continue
		}
		dayTime := nowTime + dn*86400
		// 处理小时的数据
		for hn := 0; hn < 24; hn++ {
			hourData, ok := dayData[(hn+nowHour)%24] // 注意点
			if !ok {
				continue
			}
			hourTime := dayTime + hn*3600
			// 处理分钟的数据
			for mn := 0; mn < 60; mn++ {
				num := hourData[mn]
				if num <= 0 {
					continue
				}
				minuteTime := hourTime + mn*60
				rs = append(rs, [2]int{minuteTime, num})
			}
		}
	}
	return
}

/**
 * 根据奖品的发奖计划，把设定的奖品数量放入奖品池
 * 需要每分钟执行一次
 * 【难点】定时程序，根据奖品设置的数据，更新奖品池的数据
 */
func DistributionGiftPool() int {
	totalNum := 0 //总共填充多少奖品
	nowTime := comm.NowUnix()
	giftService := services.NewGiftService()
	list := giftService.GetAll(false)
	if list != nil && len(list) > 0 {
		for _, gift := range list {
			// 是否正常状态
			if gift.SysStatus != 0 {
				continue
			}
			// 是否限量产品
			if gift.PrizeNum < 1 {
				continue
			}
			// 时间段是否正常
			if gift.TimeBegin > nowTime || gift.TimeEnd < nowTime {
				continue
			}
			// 计划数据的长度太短，不需要解析和执行
			// 发奖计划，[[时间1,数量1],[时间2,数量2]]
			if len(gift.PrizeData) < 7 {
				continue
			}
			var cronData [][2]int
			err := json.Unmarshal([]byte(gift.PrizeData), &cronData)
			if err != nil {
				log.Println("prizedata.DistributionGiftPool Unmarshal error=", err)
			} else {
				index := 0
				giftNum := 0
				for i, data := range cronData {
					ct := data[0]
					num := data[1]
					if ct <= nowTime {
						giftNum += num
						index = i + 1
					} else {
						break
					}
				}
				// 有奖品需要放入到奖品池
				if giftNum > 0 {
					incrGiftPool(gift.Id, giftNum)
					totalNum += giftNum
				}
				// 更新奖品的发奖计划
				if index > 0 {
					if index >= len(cronData) {
						cronData = [][2]int{}
					} else {
						cronData = cronData[index:]
					}
					// 更新到数据库
					str, err := json.Marshal(cronData)
					if err != nil {
						log.Println("prizedata.DistributionGiftPool Marshal(cronData) ",
							cronData, "error=", err)
					}
					cols := []string{"prize_data"}
					// 更新数据库会把缓存删掉
					err = giftService.Update(&models.LtGift{
						Id:        gift.Id,
						PrizeData: string(str),
					}, cols)
					if err != nil {
						log.Println("prizedata.DistributionGiftPool giftService.Update "+
							"error=", err)
					}
				}
			}
		}
		if totalNum > 0 {
			// 前面更新数据库把缓存删掉，这里重新预加载缓存数据
			giftService.GetAll(true)
		}
	}
	return totalNum
}

// 这里只补偿了1次，毕竟时间很短，一般补偿1次够了，如果要绝对达到预期，可以改成递归调用
func incrGiftPool(id, num int) int {
	rds := datasource.InstanceCache()
	// hincrby: 返回的是增加后的结果值，如原4，加5，返回9
	rtNum, err := redis.Int64(rds.Do("HINCRBY", giftPoolKey, id, num))
	if err != nil {
		log.Println("prizedata.incrGiftPool error=", err)
		return 0
	}
	//原来为负数，才会出现递增少于预期值的情况
	if int(rtNum) < num {
		// 递增少于预期值，补偿一次
		num2 := num - int(rtNum)
		rtNum, err = redis.Int64(rds.Do("HINCRBY", giftPoolKey, id, num2))
		if err != nil {
			log.Println("prizedata.incrGiftPool2 error=", err)
			return 0
		}
	}
	return int(rtNum)
}
